'use strict'

const EventEmitter = require('events').EventEmitter
const webpack = require('webpack')
const readline = require('readline')
const { Worker } = require('worker_threads')

const workerThreads = 16

const {
    chalk,
    fs,
    path,
    logger,
    env,
    performance
} = require('@vuepress/shared-utils')
const createClientConfig = require('../webpack/createClientConfig')
const createServerConfig = require('../webpack/createServerConfig')
const { applyUserWebpackConfig } = require('../util/index')

/**
 * Expose Build Process Class.
 */

module.exports = class Build extends EventEmitter {
    constructor(context) {
        super()
        this.context = context
        this.outDir = this.context.outDir
    }

    /**
     * Doing somthing before render pages, e.g. validate and empty output directory,
     * prepare webpack config.
     *
     * @returns {Promise<void>}
     * @api public
     */

    async process() {
        if (this.context.cwd === this.outDir) {
            throw new Error(
                'Unexpected option: "outDir" cannot be set to the current working directory'
            )
        }

        this.context.resolveCacheLoaderOptions()
        await fs.emptyDir(this.outDir)
        logger.debug('Dist directory: ' + chalk.gray(this.outDir))
        this.prepareWebpackConfig()
    }

    /**
     * Compile and render pages.
     *
     * @returns {Promise<void>}
     * @api public
     */

    async render() {
        // compile!
        performance.start()
        const stats = await compile([this.clientConfig, this.serverConfig])
        const serverBundle = require(path.resolve(
            this.outDir,
            'manifest/server.json'
        ))
        const clientManifest = require(path.resolve(
            this.outDir,
            'manifest/client.json'
        ))

        // remove manifests after loading them.
        await fs.remove(path.resolve(this.outDir, 'manifest'))

        // ref: https://github.com/vuejs/vuepress/issues/1367
        if (
            !this.clientConfig.devtool
            && (!this.clientConfig.plugins
                || !this.clientConfig.plugins.some(
                    p =>
                        p instanceof webpack.SourceMapDevToolPlugin
                        || p instanceof webpack.EvalSourceMapDevToolPlugin
                ))
        ) {
            await workaroundEmptyStyleChunk(stats, this.outDir)
        }

        // if the user does not have a custom 404.md, generate the theme's default
        if (!this.context.pages.some(p => p.path === '/404.html')) {
            await this.context.addPage({ path: '/404.html' })
        }

        // render pages
        logger.wait('Rendering static HTML...')

        let activeWorkers = 0
        const pagePaths = []
        const pagesPerThread = this.context.pages.length / workerThreads

        for (let workerNumber = 0; workerNumber < workerThreads; workerNumber++) {
            const startIndex = workerNumber * pagesPerThread
            const pageData = this.context.pages.slice(
                startIndex,
                startIndex + pagesPerThread
            )
            const pages = pageData.map(p => ({
                path: p.path,
                frontmatter: JSON.stringify(p.frontmatter)
            }))

            const payload = {
                clientManifest: JSON.stringify(clientManifest),
                outDir: this.outDir,
                pages: Buffer.from(JSON.stringify(pages)),
                serverBundle: JSON.stringify(serverBundle),
                siteConfig: JSON.stringify(this.context.siteConfig),
                ssrTemplate: JSON.stringify(this.context.ssrTemplate),
                workerNumber,
                logLevel: logger.options.logLevel
            }

            const worker = new Worker(path.join(__dirname, './worker.js'))
            worker.postMessage(payload)
            activeWorkers++
            worker.on('message', response => {
                if (response.complete) {
                    pagePaths.concat(response.filePaths)
                }
                if (response.message) {
                    logger.wait(response.message)
                }
            })
            worker.on('error', error => {
                console.error(
                    logger.error(
                        chalk.red(`Worker #${workerNumber} sent error: ${error}\n\n${error.stack}`),
                        false
                    )
                )
            })
            worker.on('exit', code => {
                activeWorkers--
                if (code === 0) {
                    logger.success(`Worker ${workerNumber} completed successfully.`)
                } else {
                    logger.error(
                        chalk.red(`Worker #${workerNumber} sent exit code: ${code}`),
                        false
                    )
                }
                if (activeWorkers === 0) {
                    // DONE.
                    readline.clearLine(process.stdout, 0)
                    readline.cursorTo(process.stdout, 0)
                    const relativeDir = path.relative(this.context.cwd, this.outDir)
                    logger.success(
                        `Generated static files in ${chalk.cyan(relativeDir)}.`
                    )
                    const { duration } = performance.stop()
                    logger.success(
                        `It took a total of ${chalk.cyan(
                            `${duration}ms`
                        )} to run the ${chalk.cyan('vuepress build')}.`
                    )
                    console.log()
                }
            })
        }

        await this.context.pluginAPI.applyAsyncOption('generated', pagePaths)
    }

    /**
     * Prepare webpack config under build.
     *
     * @api private
     */

    prepareWebpackConfig() {
        this.clientConfig = createClientConfig(this.context).toConfig()
        this.serverConfig = createServerConfig(this.context).toConfig()

        const userConfig = this.context.siteConfig.configureWebpack
        if (userConfig) {
            this.clientConfig = applyUserWebpackConfig(
                userConfig,
                this.clientConfig,
                false
            )
            this.serverConfig = applyUserWebpackConfig(
                userConfig,
                this.serverConfig,
                true
            )
        }
    }
}

/**
 * Compile a webpack application and return stats json.
 *
 * @param {Object} config
 * @returns {Promise<Object>}
 */

function compile(config) {
    return new Promise((resolve, reject) => {
        webpack(config, (err, stats) => {
            if (err) {
                return reject(err)
            }
            if (stats.hasErrors()) {
                stats.toJson().errors.forEach(err => {
                    console.error(err)
                })
                reject(new Error(`Failed to compile with errors.`))
                return
            }
            if (env.isDebug && stats.hasWarnings()) {
                stats.toJson().warnings.forEach(warning => {
                    console.warn(warning)
                })
            }
            resolve(stats.toJson({ modules: false }))
        })
    })
}

/**
 * find and remove empty style chunk caused by
 * https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85
 * TODO remove when it's fixed
 *
 * @param {Object} stats
 * @param {String} outDir
 * @returns {Promise<void>}
 */

async function workaroundEmptyStyleChunk(stats, outDir) {
    const styleChunk = stats.children[0].assets.find(a => {
        return /styles\.\w{8}\.js$/.test(a.name)
    })
    if (!styleChunk) return
    const styleChunkPath = path.resolve(outDir, styleChunk.name)
    const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8')
    await fs.remove(styleChunkPath)
    // prepend it to app.js.
    // this is necessary for the webpack runtime to work properly.
    const appChunk = stats.children[0].assets.find(a => {
        return /app\.\w{8}\.js$/.test(a.name)
    })
    const appChunkPath = path.resolve(outDir, appChunk.name)
    const appChunkContent = await fs.readFile(appChunkPath, 'utf-8')
    await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent)
}
